/*
 * Copyright 2011 Vlad Goldman.  All rights reserved.
  
   This file is part of GooOutSync - Google Outlook Sync application
  
    Project page: http://goooutsync.googlecode.com

    GooOutSync is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    GooOutSync is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with GooOutSync.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.vgold.goooutsync;

import java.io.IOException;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.crypto.SecretKey;

import org.apache.log4j.Logger;

import com.google.gdata.client.Query;
import com.google.gdata.client.Service.GDataRequest;
import com.google.gdata.client.contacts.ContactsService;
import com.google.gdata.data.Link;
import com.google.gdata.data.contacts.ContactEntry;
import com.google.gdata.data.contacts.ContactFeed;
import com.google.gdata.data.contacts.ContactGroupEntry;
import com.google.gdata.data.extensions.ExtendedProperty;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ContentType;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.common.util.Base64;
import com.vgold.goooutsync.AContact.Fields;

public class GoogleContacts {
	private static final long GOOGLE_CONNECTION_TIMEOUT = 1000L * 60 * 5;
	private static final long GOOGLE_RETRY_MILLIS = 1000L;
	private static final int GOOGLE_RETRY_TIMES = 10;
	public static final int GOOGLE_MAX_RESULTS = 10000;
	private static final String GOOGLE_HTTPS_URL = "https://www.google.com";
	private static final String GOOGLE_HTTP_URL = "http://www.google.com";
	private static final String CONTACTS_FEED_POSTFIX = "/full";
	private static final String FEEDS_DEFAULT_PART = "/m8/feeds/contacts/";
	private String credentialsFile = "goooutsync.keystore";
	private Logger logger;
	private ContactsService contactsService;
	List<ContactEntry> entries;
	URL feedUrl;
	String user;
	String password;
	SortedMap<String, GoogleContact> googleContactList;
	Iterator contactListIterator;
	SecretKey key;
	private GoogleCredentials googleCredentials;
	private GooOutSyncStatus gooOutSyncStatus;

	private GooOutSyncPreferences preferences;

	public GoogleContacts() throws GooOutSyncException {
		preferences = GooOutSyncPreferences.getGooOutSyncPreferences();
		googleCredentials = GoogleCredentials.getGoogleCredentials();
		gooOutSyncStatus = GooOutSyncStatus.getGooOutSyncStatus();
		googleContactList = new TreeMap<String, GoogleContact>();
		logger = Logger.getLogger(GoogleContacts.class);
		try {
			if (googleCredentials.getUser().isEmpty()
					|| googleCredentials.getPassword().isEmpty())
				throw new GooOutSyncException(
						"Google Account user or password is not set");
			setCredentials(googleCredentials.getUser(),
					googleCredentials.getPassword());
			logger.info("Feed URL:" + feedUrl);
			feedUrl = new URL(GOOGLE_HTTPS_URL + FEEDS_DEFAULT_PART + user
					+ CONTACTS_FEED_POSTFIX);
			logger.debug("URL created:" + feedUrl);
			contactsService = new ContactsService("contact-retriever");
			setProxyDetails();
			logger.debug("Contacts Service created:");
			logger.debug("Authenticating...");
			logger.debug("With user:" + user + " password:" + password);
			contactsService.setUserCredentials(user, password);
			logger.debug("Credentials set");
			// Send the request and receive the response:
			Query myQuery = new Query(feedUrl);
			myQuery.setMaxResults(GOOGLE_MAX_RESULTS);
			ContactFeed myFeed = contactsService.query(myQuery,
					ContactFeed.class);
			logger.debug("Got feed");
			entries = myFeed.getEntries();
			logger.debug("Got " + entries.size() + " entries ");
			for (ContactEntry e : entries) {
				GoogleContact contact = new GoogleContact(e);
				googleContactList.put(e.getId(), contact);
				printContact(contact);
			}
			gooOutSyncStatus.setGoogleContacts(entries.size());
			logger.debug("Finished!");
		} catch (AuthenticationException ae) {
			logger.error(ae.getMessage());
			throw new GooOutSyncException(ae);
		} catch (ServiceException se) {
			logger.error(se.getMessage());
			throw new GooOutSyncException(se);
		} catch (MalformedURLException he) {
			logger.error(he.getMessage());
			throw new GooOutSyncException(he);
		} catch (IOException ie) {
			logger.error(ie.getMessage());
			throw new GooOutSyncException(ie);
		}

	}

	private void setProxyDetails() {
		String proxyServer = preferences
				.getPreference(GooOutSyncPreferences.Prefs.proxy_server);
		String proxyPort = preferences
				.getPreference(GooOutSyncPreferences.Prefs.proxy_port);
		if (!proxyServer.isEmpty()) {
			GooOutProxy.setManualProxy(proxyServer,
					proxyPort.isEmpty() ? "8080" : proxyPort);
			if (!preferences.getPreference(
					GooOutSyncPreferences.Prefs.proxy_user).isEmpty()) {
				String encoded = new String(
						Base64.encode(new String(
								preferences
										.getPreference(GooOutSyncPreferences.Prefs.proxy_user)
										+ ":"
										+ preferences
												.getPreference(GooOutSyncPreferences.Prefs.proxy_password))
								.getBytes()));
				String base64encodedCredentials = "Basic " + encoded;
				contactsService.getRequestFactory().setPrivateHeader(
						"Proxy-Authorization", base64encodedCredentials);
			}
		}
	}

	private void printContact(GoogleContact contact) {
		Iterator<Fields> iterator = contact.keySet().iterator();
		while (iterator.hasNext()) {
			Object key = iterator.next();
			logger.info(key + " => " + contact.get(key));
		}
		logger.info("MODIFIED => " + contact.getModified());
		/*
		 * logger.info("IM addresses:"); for (Im im : contact.getImAddresses())
		 * { logger.info("  " + im.getAddress()); if (im.getLabel() != null) {
		 * logger.info(" label:" + im.getLabel()); } if (im.getRel() != null) {
		 * logger.info(" rel:" + im.getRel()); } if (im.getProtocol() != null) {
		 * logger.info(" protocol:" + im.getProtocol()); } if (im.getPrimary())
		 * { logger.info(" (primary) "); } logger.info("\n"); }
		 * 
		 * logger.info("Phone numbers:"); for (PhoneNumber phone :
		 * contact.getPhoneNumbers()) { logger.info("  " +
		 * phone.getPhoneNumber()); if (phone.getRel() != null) {
		 * logger.info(" rel:" + phone.getRel()); } if (phone.getLabel() !=
		 * null) { logger.info(" label:" + phone.getLabel()); } if
		 * (phone.getPrimary()) { logger.info(" (primary) "); }
		 * logger.info("\n"); }
		 * 
		 * logger.info("Addressses:"); for (PostalAddress address :
		 * contact.getPostalAddresses()) { logger.info("  " +
		 * address.getValue()); if (address.getRel() != null) {
		 * logger.info(" rel:" + address.getRel()); } if (address.getLabel() !=
		 * null) { logger.info(" label:" + address.getLabel()); } if
		 * (address.getPrimary()) { logger.info(" (primary) "); }
		 * logger.info("\n"); } logger.info("Organizations:"); for (Organization
		 * organization : contact.getOrganizations()) { logger.info(" Name: " +
		 * organization.getOrgName().getValue()); if (organization.getOrgTitle()
		 * != null) { logger.info(" Title: " +
		 * organization.getOrgTitle().getValue()); } if (organization.getRel()
		 * != null) { logger.info(" rel:" + organization.getRel()); } if
		 * (organization.getLabel() != null) { logger.info(" label:" +
		 * organization.getLabel()); } if (organization.getPrimary()) {
		 * logger.info(" (primary) "); } logger.info("\n"); }
		 * logger.info("Groups:"); for (GroupMembershipInfo group :
		 * contact.getGroupMembershipInfos()) { logger.info("  Id: " +
		 * group.getHref() + " Deleted? " + group.getDeleted()); }
		 * logger.info("Extended Properties:"); for (ExtendedProperty property :
		 * contact.getExtendedProperties()) { if (property.getValue() != null) {
		 * logger.info("  " + property.getName() + "(value) = " +
		 * property.getValue()); } else if (property.getXmlBlob() != null) {
		 * logger.info("  " + property.getName() + "(xmlBlob)= " +
		 * property.getXmlBlob().getBlob()); } } if
		 * (contact.getLink("http://schemas.google.com/contacts/2008/rel#photo",
		 * "image/*") != null) { String photoLink = contact.getLink(
		 * "http://schemas.google.com/contacts/2008/rel#photo",
		 * "image/*").getHref(); logger.info("Photo Link:" + photoLink); } //
		 * logger.info("Edit-Photo link: " + contact.getLink( //
		 * "http://schemas.google.com/contacts/2008/rel#edit-photo",
		 * "image/*").getHref() ); //logger.info("Self link: " +
		 * contact.getSelfLink().getHref()); //logger.info("Edit link: " +
		 * contact.getEditLink().getHref());
		 * logger.info("-------------------------------------------\n");
		 */
	}

	/**
	 * Prints the contents of a GroupEntry to System.err
	 * 
	 * For origin of this function, see comments for printContact() above.
	 * 
	 * Tip o' the hat to Google.
	 * 
	 * @param groupEntry
	 *            The GroupEntry to display
	 */
	private void printGroup(ContactGroupEntry groupEntry) {
		logger.info("Id: " + groupEntry.getId());
		logger.info("Group Name: " + groupEntry.getTitle().getPlainText());
		logger.info("Last Updated: " + groupEntry.getUpdated());
		logger.info("Extended Properties:");
		for (ExtendedProperty property : groupEntry.getExtendedProperties()) {
			if (property.getValue() != null) {
				logger.info("  " + property.getName() + "(value) = "
						+ property.getValue());
			} else if (property.getXmlBlob() != null) {
				logger.info("  " + property.getName() + "(xmlBlob) = "
						+ property.getXmlBlob().getBlob());
			}
		}
		logger.info("Self Link: " + groupEntry.getSelfLink().getHref());
		logger.info("Edit Link: " + groupEntry.getEditLink().getHref());
		logger.info("-------------------------------------------\n");
	}

	public void updateGoogleContact(GoogleContact gContact)
			throws ServiceException, IOException, GooOutSyncException,
			InterruptedException {
		ContactEntry contact = gContact.getContactEntry();
		URL editUrl = new URL(contact.getEditLink().getHref());
		for (int retry = 1; retry <= GOOGLE_RETRY_TIMES; retry++) {
			try {
				contactsService.update(editUrl, contact);
				return;
			} catch (ConnectException e) {
				Thread.sleep(GOOGLE_RETRY_MILLIS);
				logger.debug("Connection refused, reconnecting... Retry #"
						+ retry);
			}
		}
		throw new GooOutSyncException("Unable to connect");
	}

	public GoogleContact createGoogleContact(AContact refContact)
			throws GooOutSyncException, ServiceException, IOException {
		GoogleContact newGoogleContact = new GoogleContact();
		newGoogleContact.updateWith(refContact);
		ContactEntry newContactEntry;
		try {
			newContactEntry = createContact(newGoogleContact.getContactEntry());
		} catch (InterruptedException e) {
			logger.error(e.getMessage());
			throw new GooOutSyncException(e);
		}
		newGoogleContact.updateField(Fields.GOOID, newContactEntry.getId());

		return newGoogleContact;
	}

	public ContactEntry createContact(ContactEntry contact)
			throws ServiceException, IOException, GooOutSyncException,
			InterruptedException {
		// Ask the service to insert the new entry
		for (int retry = 1; retry <= GOOGLE_RETRY_TIMES; retry++) {
			try {
				ContactEntry newContactEntry = contactsService.insert(feedUrl,
						contact);
				return newContactEntry;
			} catch (ConnectException e) {
				Thread.sleep(GOOGLE_RETRY_MILLIS);
				logger.debug("Connection refused, reconnecting... Retry #"
						+ retry);
			}
		}
		throw new GooOutSyncException("Unabale to connect to Google Service");
	}

	public void deleteGoogleContact(GoogleContact gContact)
			throws ServiceException, IOException {
		ContactEntry cEntry = gContact.getContactEntry();
		cEntry.delete();
	}

	public static void deleteContactEntry(ContactEntry entryToDelete)
			throws ServiceException, IOException {
		entryToDelete.delete();
	}

	public void updateContactPhoto(ContactEntry contact, byte[] photoData)
			throws ServiceException, IOException {
		Link photoLink = contact.getContactPhotoLink();
		URL photoUrl = new URL(photoLink.getHref());

		GDataRequest request = contactsService.createRequest(
				GDataRequest.RequestType.UPDATE, photoUrl, new ContentType(
						"image/jpeg"));
		request.setEtag(photoLink.getEtag());
		OutputStream requestStream = request.getRequestStream();
		requestStream.write(photoData);
		request.execute();
	}

	public void addContactPhoto(ContactEntry contact, byte[] photoData)
			throws ServiceException, IOException {
		Link photoLink = contact.getContactPhotoLink();
		URL photoUrl = new URL(photoLink.getHref());

		GDataRequest request = contactsService.createRequest(
				GDataRequest.RequestType.UPDATE, photoUrl, new ContentType(
						"image/jpeg"));
		OutputStream requestStream = request.getRequestStream();
		requestStream.write(photoData);

		request.execute();
	}

	public void deleteContactPhoto(ContactEntry contact)
			throws ServiceException, IOException {
		Link photoLink = contact.getContactPhotoLink();
		URL photoUrl = new URL(photoLink.getHref());
		contactsService.delete(photoUrl, photoLink.getEtag());
	}

	public void resetIterator() {
		contactListIterator = googleContactList.keySet().iterator();
	}

	public GoogleContact getNext() {
		if (contactListIterator.hasNext()) {
			Object key = contactListIterator.next();
			logger.info(key + " => " + googleContactList.get(key));
			return (googleContactList.get(key));
		}
		return null;
	}

	public boolean hasNext() {
		return contactListIterator.hasNext();
	}

	public void processed(String key) {
		googleContactList.remove(key);
	}

	public boolean containsKey(String key) {
		return googleContactList.containsKey(key);
	}

	public GoogleContact get(String key) {
		return googleContactList.get(key);
	}

	public void setCredentials(String user, String password) {
		this.user = user;
		this.password = password;
	}

	public void deleteAllGoogleContactsWithOutloookID() {
		try {
			logger.debug("Deleting Google Contacts: " + entries.size()
					+ " entries ");
			// At this point, we have the feed. Compare to the previous post.
			List<ExtendedProperty> extendedProperties;
			contactsLoop: for (ContactEntry cEntry : entries) {
				if (cEntry.hasExtendedProperties()) {
					extendedProperties = cEntry.getExtendedProperties();
					for (ListIterator<ExtendedProperty> iter = (ListIterator<ExtendedProperty>) extendedProperties
							.listIterator(); iter.hasNext();) {
						ExtendedProperty extendedProperty = iter.next();
						if (extendedProperty.getName().equals("OUTID")) {
							logger.info("Deleting entry with Outlook ID: "
									+ cEntry.getId());
							deleteContactEntry(cEntry);
							continue contactsLoop;
						}
					}
				}

			}
			logger.debug("Finished!");
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (AuthenticationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ServiceException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
